import numpy as np
import sys
sys.path.append("../")
from common.metrics import r2_score

class LinearRegression:
    def __init__(self):
        """初始化Linear Regression模型"""
        self.coef_ = None
        self.intercept_ = None
        self._theta = None

    def fit_normal(self, X_train, y_train):
        """根据训练数据集X_train， y_train训练Linear Regression模型"""
        assert X_train.shape[0] == y_train.shape[0], "训练数据集中的样本行数和结果标签的行数必须相等"

        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        self._theta = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y_train)
        self.intercept_ = self._theta[0]
        self.coef_ = self._theta[1:]

        return self

    def fit_gradient_descent(self, X_train, y_train, eta=0.01, n_iters=1e4):
        """根据训练数据集X_train，y_train，使用梯度下降法训练线性回归模型"""
        assert X_train.shape[0] == y_train.shape[0], "训练数据集中的样本行数和结果标签的行数必须相等"
        def J(theta, X_b, y):
            try:
                return np.sum((y - X_b.dot(theta)) ** 2) / len(y)
            except:
                return float('inf')

        def dJ(theta, X_b, y):
            # res = np.empty(len(theta))
            # res[0] = np.sum(X_b.dot(theta) - y)
            # for i in range(1, len(theta)):
            #     res[i] = (X_b.dot(theta) - y).dot(X_b[:, i])
            # return res * 2 / len(X_b)
            return X_b.T.dot(X_b.dot(theta) - y) * 2. / len(X_b)

        def gradient_descent(X_b, y, initial_theta, eta=0.01, n_iters=1e4, epsilon=1e-8):
            theta = initial_theta
            i_iter = 0
            while i_iter < n_iters:
                gradient = dJ(theta, X_b, y)
                last_theta = theta

                # 矩阵中是没有这种语法的，一个大坑，需要小心
                # theta -= eta * gradient
                theta = theta - eta * gradient
                if abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon:
                    break
                i_iter += 1
            return theta

        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])
        self._theta = gradient_descent(X_b, y_train, initial_theta, eta, n_iters)
        self.intercept_ = self._theta[0]
        self.coef_ = self._theta[1:]

        return self

    def fit_stochastic_gradient_descent(self, X_train, y_train, epochs=5, t0=5, t1=50):
        """根据训练数据集X_train，y_train，使用梯度下降法训练线性回归模型"""
        assert X_train.shape[0] == y_train.shape[0], "训练数据集中的样本行数和结果标签的行数必须相等"
        assert epochs >= 1, "样本迭代次数必须要大于等于1次"

        # 随机取一行元素进行
        def dJ_sgd(theta, X_b_i, y_i):
            return X_b_i.T.dot(X_b_i.dot(theta) - y_i) * 2.
        def stochastic_gradient_descent(X_b, y, initial_theta, epochs):
            def learning_rate(t):
                return t0 / (t + t1)

            theta = initial_theta
            m = len(X_b)

            for cur_iter in range(epochs):
                # 先将索引打乱
                indexes = np.random.permutation(m)
                # 获得一个行随机排列的X矩阵
                X_b_new = X_b[indexes]
                # 获得一个随机排列的y向量
                y_new = y[indexes]
                for i in range(m):
                    gradient = dJ_sgd(theta, X_b_new[i], y_new[i])
                    theta = theta - learning_rate(cur_iter * m + i) * gradient
            return theta

        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])
        self._theta = stochastic_gradient_descent(X_b, y_train, initial_theta, epochs)
        self.intercept_ = self._theta[0]
        self.coef_ = self._theta[1:]
        return self

    def predict(self, X_predict):
        """给定待预测数据集X_predict，返回X_predict的结果向量"""
        assert self.intercept_ is not None and self.coef_ is not None, "必须先调用fit方法传入训练数据"
        assert X_predict.shape[1] == len(self.coef_), "预测数据集的特征数必须要和系数的个数相等"

        X_b = np.hstack([np.ones((len(X_predict), 1)), X_predict])
        return X_b.dot(self._theta)

    def score(self, X_test, y_test):
        """根据测试数据集X_test和y_test确定当前模型的准确度"""

        y_predict = self.predict(X_test)
        return r2_score(y_test, y_predict)

    def __repr__(self):
        return "LinearRegression()"

